
As I have been playing around with Gearsystem on Ubuntu Touch lately, I decided it was time to tackle a way to save the game. If you are unfamiliar, you can check out my previous posts about Gearsystem, but it is an emulator that was originally written by Ignacio Sanchez[1] and then ported to Ubuntu Touch by Ryan Pattison[2]. This emulator allows you to play SEGA Game Gear and Master System games, and newer versions of this emulator allow you to play other systems as well.
One might wonder, after reading that statement, why I don't update this fork to use the latest emulator. I could, but it wouldn't be easy for a fellow with low end programming skills like me. This older version of Gearsystem was written to use QT/QML, which functions nicely with Ubuntu Touch, while the newer versions do not follow that scheme, and would require a lot more custom work on my end.
Anyhow, if you are familiar with playing ROMs of games in emulators, then you know a really key feature is the ability to save the game. Some cartridges, such as (in this case, for a SEGA Master System) Phantasy Star or Crystal Warriors, have a built in save function that is part of the game. This is called persistent memory or peristent RAM. Other titles get around this by using a password or code that allows you to jump to later levels, like Mega Man or King's Quest. And still further, other titles just don't allow any sort of saving at all, like Captain America or Street Fighter II, where the game is designed to be played straight through in one sitting. Most emulators allow something called "Saved States" that allow you to save the game, as is, where is, and reload it at will at any moment. Other emulators have the ability to "Save RAM", which allows you to use built in game saving functions of the cartridge, and reload it later, as if you were playing on a physical console.
At first I decided to add the Save State style of game saving feature. I went so far as to create the custom buttons for it [3]. I messed with making it work for several hours [4], but it ended up being a bit more than my meager programming skills could handle, so I tabled that for now, and decided instead to work on the Save RAM feature instead. Particularly because the original author had already created a saveRAM and loadRAM feature. Unfortunately, they didn't function.
If you don't have time for the rest of the article, and just want to see the end result, you can check out my commit on gitlab [5] where I finally fixed it. If you're here for the story, well, hopefully telling it will not take as long as fixing it did!
As I mentioned, there was already a LoadRAM function, which would load the game cartrides persistent memory when the game ROM was selected and loaded. For the rest of the article, I am going to use Phantasy Star as the reference ROM, so all paths will be realative to that game. Essentially, from what I understood, there is a variable called 'defaultSavePath()' that is based on the name of the ROM you load, in this case, when you load the Phantasy Star ROM, the emulator will check '/home/phablet/.local/share/alaskalinuxuser.gearsystem/PhantasyStar.gearsystem' to see if it exists, and if it does, it loads it, because it should be the persistent RAM of the game cartridge, e.g. the saved games. This is called automatically when the ROM is first loaded, which is a plus, as I felt I didn't need to do anything here.
There was also a SaveRAM function, which, once called, would read the cartridge persistent RAM and save it to disk at '/home/phablet/.local/share/alaskalinuxuser.gearsystem/PhantasyStar.gearsystem'. The first problem with this was that there was nothing that I could find, which called it. So, I spent several hours trying to figure out if I could find the part of the code where the emulator actually wrote in game saving of games to the persisent RAM, so I could piggy back off of that function to allow the emulator to write that, and then call this SaveRAM function. Again, my meager skills didn't prove useful here, and I couldn't figure out when and where that happened, so I couldn't use that to trigger saving the RAM. I also considered some sort of timer function that just called the SaveRAM function regularly, but quickly abandoned that because it would waste resources and would be a bummer if you saved your game, then turned off the emulator, and it had not reached the timer to then save you data. Instead, I decided to make a button for it.
While certainly not ideal, as I would have prefered it to be automatic, but having a button proved to be a very easy way to call the SaveRAM function. Essentially, it would be on the user to first save their game, in game, like normal, and then to physically press the SaveRAM button to back up that save to their phone. A bit clunky, but useable. The button code was pretty easy, as I stole the code from the START button and just added a slot/signal for it in the Main.qml file:
save.pressed.connect(emu.savePressed)
save.released.connect(emu.saveReleased)
//////////////////////
ButtonPanel {
id: save
anchors.top: parent.top
myTextLabel: "SaveRAM"
y: units.gu(2)
width: parent.width
height: units.gu(4)
onPressed: click()
}
And the corresponding c++ in the GSEmulator.h header file:
void savePressed();
void saveReleased();
And in the GSEmulator.cpp file:
void GSEmulator::savePressed()
{
m_emu->save();
}
void GSEmulator::saveReleased()
{
// Do nothing
}
Being ever optimistic, I thought that if I just called the SaveRAM function, it would all work, and my task would be done for the day. Unfortunately, when called, the SaveRAM function didn't work as intended. The console log spit out an error that said:
'Failed to save RAM to: /home/phablet/.local/share/alaskalinuxuser.gearsystem/PhantasyStar.gearsystem'
Bummer, and I thought this would be easy! Looking at saveRAM function, I couldn't see anything wrong with it, though my skills are somewhat limited. The actual call was first to EmulationRunner::save(), which looked like this:
void EmulationRunner::save()
{
m_lock.lock();
QString path = defaultSavePath();
if (!path.isNull()) {
qDebug() << "Saving Game to: " << path;
if (not m_core.SaveRam(path.toStdString().c_str())) {
qDebug() << "Failed to save ram to: " << path;
}
} else {
qDebug() << "No Game Loaded to Save";
}
m_lock.unlock();
}
And this function calls core.SaveRam, from GearSystemCore.cpp, which looked like this:
bool GearsystemCore::SaveRam(const char* szPath)
{
if (m_pCartridge->IsReady() && IsValidPointer(m_pMemory->GetCurrentRule()) && m_pMemory->GetCurrentRule()->PersistedRAM())
{
Log("Saving RAM...");
using namespace std;
char path[512];
if (IsValidPointer(szPath))
{
strcpy(path, szPath);
strcat(path, "/");
strcat(path, m_pCartridge->GetFileName());
}
else
{
strcpy(path, m_pCartridge->GetFilePath());
}
strcat(path, ".gearsystem");
Log("Save file: %s", path);
ofstream file(path, ios::out | ios::binary);
m_pMemory->GetCurrentRule()->SaveRam(file);
Log("RAM saved");
}
return false;
}
Hmmm.... Odd, wonder why it doesn't work? So I decided to add qdebug statements in to get more logging. I started with this:
bool GearsystemCore::SaveRam(const char* szPath)
{
qDebug() << "SaveRam called.";
if (m_pCartridge->IsReady() && IsValidPointer(m_pMemory->GetCurrentRule()) && m_pMemory->GetCurrentRule()->PersistedRAM())
Just to check if the function was actually ever called. In the console, it would return that phrase, 'SaveRam called.', so I knew the function was being called. Then I decided to put a qdebug statement in the first if statement, to make sure it actually decided that the if statement was true and would actually try to save the RAM to file, like so:
if (m_pCartridge->IsReady() && IsValidPointer(m_pMemory->GetCurrentRule()) && m_pMemory->GetCurrentRule()->PersistedRAM())
{
Log("Saving RAM...");
qDebug() << "SaveRam cartridge is ready.";
/////////And furth down, at the end of the if statement///////
}
qDebug() << "SaveRam cartridge not ready.";
return false;
I figured this would show me the cartridge was ready when the if statement was true, and would show me the cartridge not ready warning when the if statement was false. To be honest, I expected the false output. Oddly, I got both?! Wait, then the if statement was true, or it wouldn't return the cartridge is ready line, and yet is still gave the cartridge is not ready answer also?! I had to think about this for a while, but then it hit me! There is a 'return false' statement if the cartridge is not ready, but there was no 'return true' statement if it did save the RAM to disk! So, I added one:
m_pMemory->GetCurrentRule()->SaveRam(file);
Log("RAM saved");
return true;
}
qDebug() << "SaveRam cartridge not ready.";
return false;
}
Ah-ha! Now when I pushed the SaveRAM button, the console would return 'SaveRam cartridge is ready.', and not say that the function failed! However, it didn't actually write the file. This made my head hurt, and I tried web searching the code snippets over and over to find a problem with them, and I kept coming back to the conclusion, in my weak programming skills, that this code should just work. So why didn't it?
While taking a break, I started talking with my brother, who is very intelligent, and he suggested that I check the path to make sure it is right and readable. It was this gem of wisdom that eventually lead me to the problem. I put in a bunch of qdebug statements to read the path the emulator was trying to write to throughout the save process and found something interesting... my code looked like this:
bool GearsystemCore::SaveRam(const char* szPath)
{
qDebug() << "SaveRam called.";
if (m_pCartridge->IsReady() && IsValidPointer(m_pMemory->GetCurrentRule()) && m_pMemory->GetCurrentRule()->PersistedRAM())
{
Log("Saving RAM...");
qDebug() << "SaveRam cartridge is ready.";
using namespace std;
char path[512];
if (IsValidPointer(szPath))
{
qDebug() << "SaveRam isvalidpointer.";
strcpy(path, szPath);
strcat(path, "/");
strcat(path, m_pCartridge->GetFileName());
qDebug() << path;
}
else
{
qDebug() << "SaveRam notvalidpointer.";
strcpy(path, m_pCartridge->GetFilePath());
qDebug() << path;
}
strcat(path, ".gearsystem");
qDebug() << path;
Log("Save file: %s", path);
qDebug() << "SaveRam logging path.";
qDebug() << path;
ofstream file(path, ios::out | ios::binary);
qDebug() << path;
m_pMemory->GetCurrentRule()->SaveRam(file);
qDebug() << path;
Log("RAM saved");
qDebug() << "SaveRam RAM saved.";
return true;
}
qDebug() << "SaveRam cartridge not ready.";
return false;
}
As I mentioned above, it originally said 'Failed to save RAM to: /home/phablet/.local/share/alaskalinuxuser.gearsystem/PhantasyStar.gearsystem'. I thought that it was trying to write a file, PhantasyStar.gearsystem, in the directory of /home/phablet/.local/share/alaskalinuxuser.gearsystem/. But when I got the output of the qdebug paths, I realized that PhantasyStar.gearsystem should be a directory! The output was:
'/home/phablet/.local/share/alaskalinuxuser.gearsystem/PhantasyStar.gearsystem/PhantasyStar.zip.gearsystem'
Where PhantasyStar.zip.gearsystem was the file written, so named on the actual filename of the ROM you loaded, and PhantasyStar.gearsystem was the folder it was expected to be in, based on the name of the ROM without the file type! It coulnd't write to it, because the folder didn't exist! The path was 'wrong'! I did a quick and dirty test by creating the folder manually, and launching the emulator again. Low and behold, it would then work and save the file! Praise God for showing me where to go, and giving me such a helpful brother!
Well, I will cut to the chase on the next three hours, because I didn't realize that this emulator was written in c++11 instead of more modern versions like 17, so you can't use the std::filesystem commands/functions to create new directories. In the end, I added this bit of code to the EmmulationRunner.cpp file:
// For Creating directory.
#include <sys/stat.h>
#include <sys/types.h>
#include <iostream>
/////////And furth down///////
}
}
void createDirectory(const std::string& path) {
struct stat info;
if (stat(path.c_str(), &info) != 0) { // Check if the directory exists
mkdir(path.c_str(), 0777); // Create the directory with permissions
} else if (info.st_mode & S_IFDIR) {
std::cout << "Directory already exists." << std::endl;
} else {
std::cout << "A file with the same name exists." << std::endl;
}
}
void EmulationRunner::save()
{
m_lock.lock();
QString path = defaultSavePath();
if (!path.isNull()) {
qDebug() << "Saving Game to: " << path;
// make this folder now if it doesn't exist.
std::string myPath = path.toStdString();
createDirectory(myPath);
if (not m_core.SaveRam(path.toStdString().c_str())) {
qDebug() << "Failed to save ram to: " << path;
}
I also had to convert the Qstrings to std::strings for this to work, but that was pretty easy with the toStdString() bit. Technically, I should probably adjust the folder permissions, but at this point I was just happy that it all worked! Now people using the GearSystem click on Ubuntu Touch can save their games if the game has a built in save function!
Linux - keep it simple.
[1] https://github.com/drhelius/Gearsystem [2] https://gitlab.com/ferrettim/gearsystem [3] https://gitlab.com/alaskalinuxuser/gearsystem/-/commit/db13c4fcd9e69bd1b7875bdda997823c37abdac8 [4] https://gitlab.com/alaskalinuxuser/gearsystem/-/commit/cb3c8eecbd3f2a03fac426cdb3c1111d3d44780a [5] https://gitlab.com/alaskalinuxuser/gearsystem/-/commit/9751dccbb5e2edcdd92ba7b893b1fa97d6cfeba3